<?php
namespace Taeluf;
class Tester {
protected $benches;
protected $catchers = [];
protected $disabled = false;
protected $results = [];
public function __construct(){
$this->prepare();
set_error_handler([$this,'throwError']);
}
public function throwError($errno, $errstr, $errfile, $errline) {
throw new \ErrorException($errstr, $errno, 0, $errfile, $errline);
}
public function catch($exceptionClass,$strict=false){
$catcher = new Tester\ExceptionCatcher($exceptionClass);
$this->catchers[] = $catcher;
return $catcher;
}
public function throw($e){
$list = $this->catchers;
foreach ($list as $index => $cat){
if ($cat->matches($e)){
//@TODO allow one catcher to be re-used & just require that each exception be caught by at least one.
unset($this->catchers[$index]);
return true;
}
}
throw $e;
}
public function disable(){
$this->disabled = true;
}
/**
* Call `$tester->test('TestName')->compare($target,$actual)` to run a sub-test inside a test in your class
* Chaining is not required
*
* @export(Usage.test)
*/
protected function test($subTestName){
echo "****$subTestName****\n";
return $this;
}
protected function startTest($key){
echo "\n ".'inner test \''.$key.'\' starts:';
}
protected function endTest($key,$result){
echo "\n ".'inner test \''.$key.'\' ends with result: <b>'.($result ? 'true' : 'false').'</b>';
}
/**
* Get an array of method names on $this that start with 'test'
*/
protected function getTestMethods(){
$methods = get_class_methods($this);
$methods = array_map(function($key,$value) //use ($builtInMethods)
{
if ($value=='test')return null;
if (substr($value,0,4)!='test')return null;
return $value;
},array_keys($methods),array_values($methods));
return $methods;
}
protected function startOb(){
return Tester\Utility::startOb();
}
protected function endOb($ob_level){
return Tester\Utility::endOb($ob_level);
}
/**
* Run tests
*
* @param $methods an array of method names to run as tests or NULL to run all methods beginning with 'test'
*/
public function run(?array $methods = null){
if ($methods===null)$methods = $this->getTestMethods();
// $this->results...
foreach ($methods as $method){
$result = ['method'=>substr($method,4), 'error'=>null];
if ($method==NULL)continue;
$this->catchers = [];
$error = null;
$retValue = null;
$this->benchStart('test_'.$method);
$ob_level = $this->startOb();
try {
$result['returnVal'] = $this->$method();
} catch (\Throwable $t){
$result['returnVal'] = null;
$result['error'] = $t;
}
$result['output'] = $this->endOb($ob_level);
$result['bench'] = $this->benchEnd('test_'.$method);
$result['result'] = $result['returnVal'] ? true : false;
$result['disabled'] = $this->disabled;
$this->disabled = false;
if (($c=count($this->catchers))>0){
// $result = false;
$result['output'] = "---exception-fail---\n{$c} exceptions were not handled.\n----------\n".$result['output'];
}
$result['html'] = $this->htmlOutput((object)$result);
$this->results[$method] = $result;
}
return $this->results;
//now do what we do with results.
}
public function benchStart($key){
$this->benches[$key]['start'] = microtime(true);
}
public function benchEnd($key){
$end = microtime(true);
$start = $this->benches[$key]['start'];
unset($this->benches[$key]);
$diff = $end - $start;
return (object)[
'start'=>$start,
'end'=>$end,
'diff'=>$diff
];
}
public function compare($target, $actual,$strict=false){
if (!$strict&&is_string($target)&&substr($target,0,7)=='file://'){
$file = substr($target,7);
if (!is_file($file)){
throw new \Exception("{$target} is not a file. ");
}
$ext = substr($file,-4);
if ($ext=='.php'){
ob_start();
require($file);
$target=ob_get_clean();
}
else $target=file_get_contents($file);
}
if (!$strict&&is_string($actual)&&substr($actual,0,7)=='file://'){
$file = substr($actual,7);
if (!is_file($file)){
throw new \Exception("{$actual} is not a file. ");
}
$ext = substr($file,-4);
if ($ext=='.php'){
ob_start();
require($file);
$actual=ob_get_clean();
}
else $actual=file_get_contents($file);
}
if (!$strict&&is_string($target)&&is_string($actual)){
$target = trim($target);
$actual = trim($actual);
}
$pass = false;
if ($strict&&$target===$actual)$pass = true;
else if ($target==$actual)$pass = true;
$target = $this->comparisonOutput($target);
$actual = $this->comparisonOutput($actual);
// echo "Strict: ".($strict ? 'true' : 'false');
if ($pass)echo "+++pass+++";
else echo "---fail---";
if ($strict)echo " strict comparison";
echo "\n";
echo "Target:\n{$target}";
echo "\n--\n";
echo "Actual:\n{$actual}";
echo "\n--------\n";
return $pass;
}
public function comparisonOutput($value){
if (is_object($value)){
return "Object of class ".get_class($value);
}
if (is_array($value)){
$pr = var_export($value,true);
$oneLine = implode(" ",array_map('trim',explode("\n",$pr)));
$value = substr($oneLine,0,200);
if (strlen($oneLine)>200)$value .= '...';
return $value;
}
if ($value===true)return 'true';
else if ($value===false)return 'false';
return $value;
}
//@export_start(Example.ModifyOutput)
public function htmlOutput($details){
ob_start();
$successStatement = $details->result ? '<span style="color:green;">success</span>' : '<span style="color:red;">fail</span>';
if ($details->error!=null)$successStatement = '<strong style="color:blue;">error</strong>';
if ($details->disabled===true)$successStatement = '<strong style="color:orange;">disabled</strong>';
$diff = $details->bench->diff;
if ($diff < 0.0001)$diff = '';
else $diff = 'in '.number_format($diff*1000,3).'ms';
echo "<details>\n <summary><b>".$details->method.":</b> ".$successStatement." {$diff} </summary>\n";
// echo " <div>Time to run: ".$details->bench->diff."</div>";
echo " <div style='padding-left:4ch;white-space:pre;'>\n";
$detailsOutput = htmlentities($details->output);
$detailsLines = explode("\n",$detailsOutput);
$detailsLines = array_map(function($value){return ' '.$value;},$detailsLines);
echo implode("\n",$detailsLines);
// var_dump($detailsLines);
echo "\n </div>";
if ($details->error!=null){
echo "\n <br>\n";
echo " <div style='color:red;padding-left:4ch;white-space:pre;'>\n";
$errorOutput = $details->error;
$errorLines = explode("\n",$errorOutput);
$errorLines = array_map(function($value){return ' '.$value;},$errorLines);
echo implode("\n",$errorLines);
echo "\n </div>";
}
echo "\n</details>\n";
return ob_get_clean();
}
//@export_end(Example.ModifyOutput)
public function prepare(){
}
/**
* On a class that extends `\Taeluf\Tester`, call `ExtendingClass::runAll()` to run the tests.
*
* @deprecated this function no longer does anything.
* @export(RunTests.All)
*/
static public function runAll(){
// $tester = new static();
// $tester->run();
// return $tester;
}
/**
*
* @deprecated this function no longer does anything
*/
static public function runTests($name){
// $tester = new static();
// $tester->run($name);
// return $tester;
}
/**
*
* @deprecated this function no longer does anything
*/
static public function runAllToFile($filePath,$andPrint = true){
// ob_start();
// $tester = static::runAll();
// $output = ob_get_clean();
// if ($andPrint)echo $output;
// file_put_contents($filePath, $output);
//
// return $tester;
}
/**
* Call `Taeluf\Tester::xdotoolRefreshFirefox($switchBackToCurrWindow = false)` to refresh your browser tab.
* If you're writing you're using `runAllToFile($file)`, this could come in handy.
*
* @deprecated in favor of \Taeluf\Tester\Utility::xdotoolRefreshFirefox()
* @export(Extra.RefreshBrowserTab)
*/
static public function xdotoolRefreshFirefox($switchBackToWindow = false){
$args = $switchBackToWindow ? ' y' : '';
system(__DIR__.'/reload.sh'.$args);
\Taeluf\Tester\Utility::xdotoolRefreshFirefox($switchBackToWindow);
}
}